﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.Windows.Interop;
using System.Windows;
using System.Runtime.InteropServices;
using System.Windows.Input;
using System.Xml.Linq;
using System.Xml.Serialization;
using System.IO;
using System.Xml;
using iTFF.WPF.Model.GlobalHotKeys;
using iTFF.WPF.Resources.Languages;

namespace iTFF.WPF.CP
{
    /// <summary>
    /// The Hotkeys Control Panel. With the help of Vista Thumbnailer : http://huddledmasses.org/vista-thumbnailer/
    /// </summary>
    class GlobalHotKeysControlPanel : IDisposable
    {
        #region Fields

        IntPtr _handle;
        WindowInteropHelper _windowInteropHelper;
        Window _actualWindow;
        int _currentHotKeyID;
        bool _registerAllWhenWinowInitializedPending;

        #endregion

        #region Events

        public event EventHandler<GlobalHotKeyPressedEventArgs> HotkeyPressed;

        #endregion

        #region Constructor

        public GlobalHotKeysControlPanel(Window TopLevelWindow)
        {
            _actualWindow = TopLevelWindow;
            HotKeys = new ObservableCollection<GlobalHotKey>();

            if (!_actualWindow.IsArrangeValid)
            {
                _actualWindow.SourceInitialized += new EventHandler(OnSourceInitialized);
            }
            else OnSourceInitialized(_actualWindow, null);
        }

        void OnSourceInitialized(object sender, EventArgs e)
        {
            _windowInteropHelper = new WindowInteropHelper(_actualWindow);
            _handle = _windowInteropHelper.Handle;

            HwndSource.FromHwnd(_handle).AddHook(new HwndSourceHook(WndProc));

            if (_registerAllWhenWinowInitializedPending)
                RegisterAll();
        }

        #endregion

        #region Properties

        /// <summary>
        /// The available HotKeys, registered or not
        /// </summary>
        public ObservableCollection<GlobalHotKey> HotKeys { get; set; }

        #endregion

        #region Persistence

        /// <summary>
        /// Restore the HotKeys base on the ShortcutList.xml and the Settings file
        /// </summary>
        public void RestoreGlobalHotKeys()
        {
            Dictionary<string, GlobalHotKey> scs = new Dictionary<string, GlobalHotKey>();

            // read globalhotkeyslist.xml file
            XDocument doc = XDocument.Parse(Resources.Resources.GlobalHotKeysList);

            var xmlHotKeys = doc.Elements().First().Elements().Where(e => e.Name == "hotkey")
                .Select(e => new
                {
                    Name = e.Elements("name").First().Value,
                    ResourceName = e.Elements("resourcename").First().Value,
                    HasSeparatorBefore = ParseHasSeparator(e)
                });

            // now get the hot keys from the settings
            var serializer = new XmlSerializer(typeof(GlobalHotKey));

            var tempHotkeys = new List<GlobalHotKey>();

            if (!string.IsNullOrEmpty(Settings.Default.HotKeys))
            {
                var listStr = Settings.Default.HotKeys.Split('#');

                // restore all hotkeys from settings
                foreach (string str in listStr)
                {
                    if (!string.IsNullOrEmpty(str))
                    {
                        using (var stream = new MemoryStream())
                        {
                            using (var writer = new StreamWriter(stream))
                            {
                                writer.WriteLine(str);
                                writer.Flush();
                                stream.Seek(0, SeekOrigin.Begin);

                                using (var reader = XmlReader.Create(stream))
                                {

                                    if (serializer.CanDeserialize(reader))
                                    {
                                        var hk = (GlobalHotKey)serializer.Deserialize(reader);

                                        // verify if the name is still OK
                                        if (xmlHotKeys.Any(h => h.Name == hk.Name))
                                            tempHotkeys.Add(hk);
                                    }
                                }
                            }
                        }
                    }
                }
            }

            // now create the unstored hotkeys and specifies the DisplayText for all, depending of the resource file
            foreach (var xhk in xmlHotKeys)
            {
                var hk = tempHotkeys.FirstOrDefault(h => h.Name == xhk.Name);
                if (hk == null)
                    HotKeys.Add(new GlobalHotKey()
                    {
                        Name = xhk.Name,
                        DisplayText = GetStringFromResourceName(xhk.ResourceName) ?? xhk.Name,
                        HasSeparatorBefore = xhk.HasSeparatorBefore
                    });
                else
                {
                    hk.DisplayText = GetStringFromResourceName(xhk.ResourceName) ?? xhk.Name;
                    hk.HasSeparatorBefore = xhk.HasSeparatorBefore;
                    HotKeys.Add(hk);
                }
            }


            // finally Register all hotkeys, only if the window is initialized
            if (_handle != IntPtr.Zero)
                RegisterAll();
            else
                _registerAllWhenWinowInitializedPending = true;
        }

        private string GetStringFromResourceName(string resourceName)
        {
            var t = typeof(Strings);
            var prop = t.GetProperty(resourceName);
            if (prop != null)
            {
                var val = prop.GetValue(null, null) as string;
                if (val != null)
                    return val;
            }

            return null;
        }

        /// <summary>
        /// Returns if the xml node &lt;shortcut&gt; "hasSeparatorValue" or by default false
        /// </summary>
        /// <param name="e"></param>
        /// <returns></returns>
        private bool ParseHasSeparator(XElement e)
        {
            var att = e.Attribute("hasSeparatorBefore");

            if (att != null)
            {
                bool value;
                if (bool.TryParse(att.Value, out value))
                {
                    return value;
                }
            }

            return false;
        }

        /// <summary>
        /// Saves the HotKeys in the settings file
        /// </summary>
        public void SaveGlobalHotKeys()
        {
            var serializer = new XmlSerializer(typeof(GlobalHotKey));
            var outputString = new StringBuilder();

            using (MemoryStream stream = new MemoryStream())
            {
                using (StreamReader reader = new StreamReader(stream))
                {
                    foreach (var hk in HotKeys.Where(hk => hk.IsSet))
                    {
                        var oldsize = stream.Length;

                        serializer.Serialize(stream, hk);
                        stream.Seek(oldsize, SeekOrigin.Begin);
                        outputString.Append(reader.ReadToEnd());
                        outputString.Append("#");
                    }
                }
            }

            if (outputString.Length > 0)
            {
                // remove le last separator : #
                outputString.Remove(outputString.Length - 1, 1);

                // save values to settings
                Settings.Default.HotKeys = outputString.ToString();
                Settings.Default.Save();
            }
        }

        #endregion

        #region Event Handling

        /// <summary>
        /// Override the base WndProc ... but in WPF it's the old-fashioned multi-parameter WndProc
        /// <remarks>
        /// The .Net Framework is starting to feel ridiculously cobbled together ... why on earth 
        /// should WPF apps need to register WndProc's any differently than Windows.Forms apps?
        /// </remarks>
        /// </summary>
        /// <param name="hwnd">The window handle.</param>
        /// <param name="msg">The message.</param>
        /// <param name="wParam">The high word</param>
        /// <param name="lParam">The low word</param>
        /// <param name="handled"><c>true</c> if the message was already handled.</param>
        /// <returns>IntPtr.Zero - I have no idea what this is supposed to return.</returns>
        private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            if (WM.Hotkey == msg)
            {
                var hk = GetHotkeyByID(wParam.ToInt32());
                if (hk != null)
                {
                    OnHotKeyPressed(hk);
                    handled = true;
                }
            }
            return IntPtr.Zero;
        }

        protected void OnHotKeyPressed(GlobalHotKey hk)
        {
            if (HotkeyPressed != null)
                HotkeyPressed(this, new GlobalHotKeyPressedEventArgs(_actualWindow, hk));
        }

        #endregion

        #region Private helpers

        /// <summary>
        /// Returns the HotKey base on its ID
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        private GlobalHotKey GetHotkeyByID(int id)
        {
            return HotKeys.FirstOrDefault(hk => hk.HotKeyID == id);
        }

        /// <summary>
        /// Returns the HotKey base on its name
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        private GlobalHotKey GetHotKeyByName(string name)
        {
            return HotKeys.FirstOrDefault(hk => hk.Name == name);
        }

        #endregion

        #region Registering

        /// <summary>Register a new hotkey, and add it to our collection
        /// </summary>
        /// <param name="key">A reference to the Hotkey.</param>
        /// <returns>True if the hotkey was set successfully.</returns>
        public void Register(GlobalHotKey key)
        {
            if (_handle == IntPtr.Zero) throw new InvalidOperationException("You can't register hotkeys until your Window is loaded.");

            if (key.IsSet)
            {
                if (RegisterHotKey(_handle, ++_currentHotKeyID, key.Modifiers, key.VirtualKeys))
                    key.HotKeyID = _currentHotKeyID;
                else
                {
                    throw new ApplicationException(Strings.GlobalHotKeyCPCannotRegisterShortcut);
                }
            }
        }

        /// <summary>
        /// Registers all the shortcuts that need to be saved
        /// </summary>
        private void RegisterAll()
        {
            foreach (var hk in this.HotKeys.Where(hk => hk.IsSet && hk.HotKeyID == -1))
            {
                try
                {
                    Register(hk);
                }
                catch (ApplicationException)
                {
                    // if the app can't register the shortcut, nevermind
                }
            }
        }

        /// <summary>Unregister the specified hotkey if it's in our collection
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns>True if we successfully unregister the hotkey.</returns>
        public bool Unregister(GlobalHotKey key)
        {
            return Unregister(key.HotKeyID);
        }

        /// <summary>Unregister the specified hotkey if it's in our collection
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns>True if we successfully unregister the hotkey.</returns>
        public bool Unregister(int hotKeyID)
        {
            if (GetHotkeyByID(hotKeyID) != null)
                return UnregisterHotKey(_handle, hotKeyID);
            else return false;
        }

        /// <summary>
        /// Unregister all Hot Keys. Usefull when disposing.
        /// </summary>
        public void UnregistrerAll()
        {
            var copy = new GlobalHotKey[HotKeys.Count];

            HotKeys.CopyTo(copy, 0);

            foreach (var hk in copy)
                Unregister(hk);
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            UnregistrerAll();
            _handle = IntPtr.Zero;
        }

        #endregion

        #region DllImports

        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool RegisterHotKey(IntPtr hwnd, int id, ModifierKeys fsModifiers, int vk);

        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool UnregisterHotKey(IntPtr hwnd, int id);

        #endregion
    }
}
